iT邦幫忙

2024 iThome 鐵人賽

DAY 20
0

昨天我們更新家用品所使用的 Item 模型,今天就可以來繼續整合家用品與分類和地點,並優化頁面。讓使用者在 App 中新增或編輯家用品時,可以方便地設定物品的分類和存放地點。

目標

今天的目標是:

  • 優化物品的新增與編輯頁面,讓這兩個頁面新增分類與地點的選項。
  • 優化首頁物品顯示,讓使用者在查看物品時,能夠看到物品的分類圖示與存放地點。

https://ooorito.com/wp-content/uploads/2024/09/%E9%A6%96%E9%A0%81%E7%A4%BA%E6%84%8F%E5%9C%96.webp

https://ooorito.com/wp-content/uploads/2024/09/%E6%96%B0%E5%A2%9E%E9%A0%81%E7%A4%BA%E6%84%8F%E5%9C%96-1.webp

https://ooorito.com/wp-content/uploads/2024/09/%E4%BF%AE%E6%94%B9%E9%A0%81%E7%A4%BA%E6%84%8F%E5%9C%96.webp

優化物品新增與編輯頁面

更新 DataManager

既然昨天更改了 Item 模型,就代表負責更新資料庫的 DataManager 也需要更新。我們需要將 addItem 與 updateItem 都更新,讓它們也需要儲存分類與地點資料。

// Create
func addItem(name: String, quantity: Int, price: Double, dateAdded: Date, expiryDate: Date?, category: ItemCategory, location: Location) -> Bool {
    let newItem = Item(context: container.viewContext)
    newItem.id = UUID()
    newItem.name = name
    newItem.quantity = Int16(quantity)
    newItem.isUsedUp = false
    newItem.dateAdded = dateAdded
    newItem.price = price
    newItem.usedQuantity = 0
    newItem.expiryDate = expiryDate
    newItem.category = category
    newItem.location = location
    
    return saveContext()
}

// Update
func updateItem(item: Item, name: String, quantity: Int, price: Double, dateAdded: Date, expiryDate: Date?, isUsedUp: Bool, usedQuantity: Int, category: ItemCategory, location: Location) -> Bool {
    item.name = name
    item.quantity = Int16(quantity)
    item.isUsedUp = isUsedUp
    item.dateAdded = dateAdded
    item.price = price
    item.usedQuantity = Int16(usedQuantity)
    item.category = category
    item.location = location
    return saveContext()
}

ViewModel

不管是新增物品頁還是編輯物品頁,它們的 ViewModel 更改方法都大同小異,這邊就使用新增物品頁來示範。我們需要先建立以下變數:

  • categories:儲存資料庫中所有分類資料。
  • locations:儲存資料庫中所有地點資料。
  • category:儲存使用者選取的分類。
  • location:儲存使用者選取的地點。
class AddItemViewModel: ObservableObject {
    @Published var categories: [ItemCategory] = []
    @Published var locations: [Location] = []
    @Published var category: ItemCategory?
    @Published var location: Location?

接著我們需要在 init 時呼叫 func,讓 categories 和 locations 儲存資料庫抓出來的資料。

init(dataManager: DataManager = DataManager()) {
    self.dataManager = dataManager
    fetchItemCategory()
    fetchLocation()
}

func fetchItemCategory() {
    categories = dataManager.fetchItemCategories()
}

func fetchLocation() {
    locations = dataManager.fetchLocations()
}

最後再更新 save(), 讓分類和地點也被傳遞到 dataManager.addItem 中,儲存到資料庫。

func save() {
    guard let category, let location else {
        failHandle = (isFail: true, title: "請選擇分類或地點")
        return
    }
    
    if validateAndSave(), let priceValue = Double(price) {
        // 資料驗證通過,儲存資料
        let result = dataManager.addItem(
            name: name,
            quantity: quantity,
            price: priceValue,
            dateAdded: dateAdded,
            expiryDate: shouldRemindExpiryDate ? expiryDate : nil,
            category: category,
            location: location
        )
        
        if result {
            showSuccessToast = true
        } else {
            failHandle = (isFail: true, title: "資料新增失敗")
        }
    }
}

這樣 ViewModel 就更新完畢了~

View

一樣我們已新增頁面來示範。
新增一個新的 Section ,並使用 Picker,讓使用者選取所要的分類和地點。

Section(header: Text("分類與地點")) {
    Picker("選擇分類", selection: $viewModel.category) {
        ForEach(viewModel.categories, id: \.id) { category in
            Text(category.name).tag(category as ItemCategory?)
        }
    }
    Picker("選擇地點", selection: $viewModel.location) {
        ForEach(viewModel.locations, id: \.id) { location in
            Text(location.name).tag(location as Location?)
        }
    }
}

Picker

Picker 是 SwiftUI 中的一個用來呈現多選項的元件,它可以呈現文字、圖文或 Segmented Control 讓使用者選擇一個選項。它類似於傳統 UI 中的下拉選單,常用於需要選擇單一選項的場景。
基本結構範例如下:

Picker("選擇分類", selection: $選擇的變數) {
    Text("選項1").tag(選項值)
    Text("選項2").tag(選項值)
}

在 SwiftUI 中,Picker 的選擇是通過綁定的變數(例如 @State@Binding)來進行。selection 參數指向一個綁定的變數,當用戶選擇一個選項時,這個變數會自動更新為對應的選項值。

為什麼要加上 .tag()
Picker 中,每一個選項的標籤都需要通過 .tag() 明確標識它所對應的值。這是因為 Picker 需要根據這些標籤來識別選項,並將所選的項目與 selection 綁定的變數進行比較與更新。

如果沒有加上 .tag(),SwiftUI 不知道 Picker 中每個選項對應的值是什麼,也無法將選項與 selection 的變數進行匹配。這會導致當使用者選擇一個項目時,無法正確地更新綁定的變數。

在這個例子中,.tag(category as ItemCategory?) 告訴 Picker,當用戶選擇該選項時,viewModel.category 將會更新為 category,從而正確顯示用戶的選擇。

參考資料:

優化首頁

接下來,我們要優化首頁的列表,讓使用者可以一眼看到物品的分類與存放地點。
將物品清單的顯示功能抽取成新的 ItemListView 和 ItemRow,讓程式碼更具可讀性。

原 HomeView(ContentView) 程式碼

List {
    ForEach(viewModel.items) { item in
        NavigationLink(destination: EditItemView(viewModel: EditItemViewModel(dataManager: viewModel.dataManager, item: item))) {
            HStack {
                VStack(alignment: .leading) {
                    Text(item.name)
                    if let expiryDate = item.expiryDate {
                        Text("到期日\(expiryDate)")
                            .font(.subheadline).foregroundColor(.gray)
                    }
                }
                Spacer()
                Text("數量: \(item.quantity)").font(.subheadline)
            }
        }
    }
    .onDelete(perform: viewModel.deleteItems)
}
.onAppear {
    viewModel.fetchItems()
}

這段程式碼的問題在於,List 和物品的顯示混雜在一起,這使得每次物品顯示的樣式改動都必須進行大範圍的修改。因此,我們將物品顯示的邏輯獨立出來,分為 ItemListView 和 ItemRow。

實作 ItemListView

ItemListView 負責處理物品清單的顯示,並在物品數量為 0 的情況下,顯示提示文字與圖案,告知使用者清單中尚無任何紀錄。

struct ItemListView: View {
    @ObservedObject var viewModel: ItemViewModel
    
    var body: some View {
        if (viewModel.items.count > 0) {
            List {
                ForEach(viewModel.items, id: \.id) { item in
                    NavigationLink(destination: EditItemView(viewModel: EditItemViewModel(dataManager: viewModel.dataManager, item: item))) {
                        ItemRow(item: item)
                    }
                }
                .onDelete(perform: viewModel.deleteItems)
            }
        } else {
            VStack {
                Image(systemName: "list.clipboard")
                    .resizable()
                    .frame(width: 100, height: 140)
                    .padding()
                Text("沒有紀錄")
                Text("點擊+號新增一筆")
            }
            .foregroundColor(.gray)
        }
    }
    
    private var itemFormatter: DateFormatter {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.locale = Locale.current
        return formatter
    }
}

#Preview {
    let dataManager = DataManager()
    let viewModel = ItemViewModel()
    return ItemListView(viewModel: ItemViewModel())
}

實作 ItemRow

ItemRow 是用來顯示單筆物品資料的元件,包含物品名稱、分類圖示、到期日,以及存放位置。

import SwiftUI

struct ItemRow: View {
    @ObservedObject var item: Item
    
    var body: some View {
        HStack {
            Image(systemName: item.category.iconName)
                .resizable()
                .frame(width: 40, height: 40)
                .foregroundColor(Color(hexString: item.category.categoryGroup.colorHex))
                .padding([.trailing], 20)
            
            VStack(alignment: .leading) {
                Text(item.name)
                    .font(.headline)
                if let expiryDate = item.expiryDate {
                    Text(verbatim: "到期日:\(String(describing: expiryDate.formatted(date: .abbreviated, time: .omitted)))")
                        .font(.subheadline)
                        .foregroundColor(.gray)
                } else {
                    Text(verbatim: "到期日:未設定")
                        .font(.subheadline)
                        .foregroundColor(.gray)
                }
            }
            
            Spacer()
            Text(item.location.name)
                .font(.system(size: 14, weight: .bold))
                .padding()
                .background(Color(hexString: item.location.colorHex) ?? .black)
                .foregroundColor(.white)
                .cornerRadius(10)
        }
        .padding()
    }
}

修改 HomeView

將 HomeView 中的 List 改為引用 ItemListView,並設定好其佈局限制,讓它能夠符合父容器的大小:

ItemListView(viewModel: viewModel)
    .frame(maxHeight: .infinity)
    .frame(maxWidth: .infinity)
    .onAppear {
        viewModel.fetchItems()
    }

這樣就完成了,讓我們來看看成果吧!
Day 20 成果

總結

今天的實作將分類、地點管理與物品管理完美整合,並優化使用者體驗。在首頁顯示物品時,使用者可以直接看到物品的分類與存放地點,提升管理效率。同時,我們透過元件化設計,讓程式碼更加簡潔與易讀。明天我們將實作帳務報表功能,明天見!


上一篇
Day19: SwiftUI 分類管理、地點管理與側邊欄結合,提升物品管理功能
下一篇
Day 21: SwiftUI 帳務報表 - 圓餅圖
系列文
用 SwiftUI 掌控家庭日用品庫存30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言